12. 插件

Grails提供了许多扩展点来满足你的扩展,包括从命令行接口到运行时配置引擎。以下章节详细说明了该如何着手来做这些扩展。

12.1 创建和安装插件

创建插件

创建一个Grails插件,只需要运行如下命令即可:

grails create-plugin [PLUGIN NAME]

根据你输入的名字将产生一插件工程。比如你输入 grails create-plugin example. 系统将创建一个名为 example的插件工程.

除了插件的根目录有一个所谓的“插件描述”的Groovy文件外,其他的跟一般的Grails工程结构完全一样.

将插件作为一个常规的Grails工程是有好处的,比如你可以马上用以下命令来测试你的插件:

grails run-app

由于你创建插件默认是没有 URL 映射的,因此控制器并不会马上有效.如果你的插件需要控制器,那要创建 grails-app/conf/MyUrlMappings.groovy 文件,并且在起始位置增加缺省的映射 "/$controller/$action?/$id?"().

插件描述文件本身需要符合以 GrailsPlugin 结尾的惯例并且将位于插件工程的根目录中。比如:

class ExampleGrailsPlugin {
   def version = 0.1

… }

所有插件的根目录下边都必须有此类并且还要有效,此类中定义了插件的版本和其他各式各样的可选的插件扩展点的钩子(hooks)--即插件预留的可以扩展的接口.

通过以下特殊的属性,你还可以提供插件的一些额外的信息:

Quartz Grails plugin为例:

class QuartzGrailsPlugin {
    def version = "0.1"
    def author = "Sergey Nebolsin"
    def authorEmail = "nebolsin@gmail.com"
    def title = "This plugin adds Quartz job scheduling features to Grails application."
    def description = '''
Quartz plugin allows your Grails application to schedule jobs to be
executed using a specified interval or cron expression. The underlying
system uses the Quartz Enterprise Job Scheduler configured via Spring,
but is made simpler by the coding by convention paradigm.
'''
    def documentation = "http://grails.org/Quartz+plugin"

… }

插件的安装和发布

要发布插件,你需要一个命令行窗口,并且进入到插件的根目录,输入:

grails package-plugin

这将创建一个 grails- +插件名称+版本的zip文件. 以先前的example插件为例,这个文件名是 grails-example-0.1.zip. package-plugin 命令还将生成 plugin.xml f在此文件中包含机器可读的插件信息,比如插件的名称、版本、作者等等。

产生了可以发布的插件文件以后(zip文件),进入到你自己的Grails工程的根目录,输入:

grails install-plugin /path/to/plugin/grails-example-0.1.zip

如果你的插件放在远程的Http服务器上,你也可以这样:

grails install-plugin http://myserver.com/plugins/grails-example-0.1.zip

注意被排除的组件

尽管 create-plugin 命令为您创建某些文件,以便插件能做为Grails应用运行,但是当打包插件的时候不是所有的文件都会在含在里面. 以下是通过package-plugin创建时,不包含的文件和目录:

如果你希望创建包含 WEB-INF 目录的组建,那么建议你使用 _Install.groovy 脚本文件 (covered later),这个脚本文件之后会解释;当安装一个插件提供这些组件时,这个脚本文件会被执行。 此外,除了用 UrlMappings.groovy之外,也允许你使用包括 UrlMappings 名字来定义不同的名称,例如 FooUrlMappings.groovy

12.2 插件仓库

在Grails插件的存储仓库(Repository)发布插件

更好的发布插件的方式是将其发布到Grails插件的存储仓库. 这样通过 list-plugins 命令就可以看到你的插件了:

grails list-plugins

此命令将列出Grails插件存储库的所有插件,当然了也可以用 plugin-info 来查看指定插件的信息:

grails plugin-info [plugin-name]

这将输出更多的详细信息,这些信息都是维护在插件描述文件中的。

如果你创建了一个Grails插件,你可以访问 创建插件,这里详细说明了如何在容器中发布你的插件。

当你有访问Grails插件仓库的权限时,要发行你的插件,只需要简单执行 release-plugin 即可:

grails release-plugin

这将自动地将改动提交到SVN和创建标签(svn的tagging),并且通过 list-plugins 命令你可以看到这些改动.

配置附加库

默认情况下,您使用的 list-plugins, install-plugin and release-plugin 命令都指向 http://plugins.grails.org。

然而, 要配置多个插件仓库,您可以使用grails-app/conf/BuildSettings.groovy 文件:

grails.plugin.repos.discovery.myRepository="http://svn.codehaus.org/grails/trunk/grails-test-plugin-repo"
grails.plugin.repos.distribution.myRepository="https://svn.codehaus.org/grails/trunk/grails-test-plugin-repo"

Repositories are split into those used for discovery over HTTP and those used for distribution, typically over HTTPS. 如果你想在多个项目中使用相同的设置,你可以把这些配置到 USER_HOME/.grails/settings.groovy

一旦使用了 list-plugins, install-plugin and plugin-info 命令将会自动处理最新配置的插件库。如果你只想把插件库中的插件列表列出来,你可以使用别名:

grails list-plugins -repository=myRepository

此外,如果你想和配置好的插件包一起发布插件,你可以用 release-plugin 命令:

grails release-plugin -repository=myRepository

12.3 理解插件的结构

如前所提到的,一个插件除了包含一个插件描述文件外,几乎就是一个常规的Grails应用。尽管如此,当安装以后,插件的结构还是有些许的差别。比如一个插件目录的结构如下:

+ grails-app
     + controllers
     + domain
     + taglib
     etc.
 + lib
 + src
     + java
     + groovy
 + web-app
     + js
     + css

从本质上讲,当一个插件被安装到Grails工程以后, grails-app 下边的内容将被拷贝到以 plugins/example-1.0/grails-app(以example为例)目录中. 这些内容 不会 被拷贝到工程的源文件主目录,即插件永远不会跟工程的主目录树有任何接口上的关系。.

然而,那些在特定插件目录中 web-app 目录下的静态资源将会被拷贝到主工程的 plugins 目录下. 比如 web-app/plugins/example-1.0/js.

因此,要从正确的地方引用这些静态资源也就成为插件的责任。比如,你要在GSP中引用一个JavaScript文件,你可以这样:

<g:createLinkTo dir="/plugins/example/js" file="mycode.js" />

这样做当然可以,但是当你开发插件并且单独运行插件的时候,将产生相对链接(link)的问题.

为了应对这种变化即不管插件是单独运行还是在Grails应用中运行,特地新增一个特别的 pluginContextPath 变量,用法如下:

<g:createLinkTo dir="${pluginContextPath}/js" file="mycode.js" />

这样在运行期间 pluginContextPath 变量将会等价于/ 或 /plugins/example 这取决于插件是单独运行还是被安装在Grails应用中

在lib和 src/java 以及 src/groovy 下的Java、Groovy代码将被编译到当前工程的 web-app/WEB-INF/classes 下边,因此在运行时也不会出现类找不到的问题.

12.4 提供基础的工件

增加新的脚本

在插件的scripts目录下可以增加新的Gant相关的脚本:

+ MyPlugin.groovy
   + scripts     <-- additional scripts here
   + grails-app
        + controllers
        + services
        + etc.
    + lib

增加新的控制器,标签库或者服务

grails-app 相关的目录树下,可以增加新的控制器、标签库、服务等,不过要注意:当插件被安装后将从其被安装的地方加载,而不是被拷贝到当前主应用工程的相应目录。.

+ ExamplePlugin.groovy
   + scripts
   + grails-app
        + controllers  <-- additional controllers here
        + services <-- additional services here
        + etc.  <-- additional XXX here
    + lib

Providing Views, Templates and View resolution

提供控制器的插件也会提供默认的视图。通过插件模块化您的应用是个很好的途径。Grails视图处理机制的工作原理是首先查看应用中被安装的视图,如果失败将视图查找插件中的视图。

比如有一个 AmazonGrailsPlugin 插件提供一个叫 BookController 的控制器,如果执行了 list 将会首先查找 grails-app/views/book/list.gsp 这个视图,如果失败,将会在插件里查找相同名称的视图。

但是,如果视图使用了模板,同时插件也提供了这个视图,那么必须使用以下的语法:

<g:render template="fooTemplate" contextPath="${pluginContextPath}"/>

注意 pluginContextPath 变量做为 contextPath 属性值的用法。如果没有指定这个属性,Grails将在应用中的模板中查找。

Excluded Artefacts

默认的,when packaging a plug-in,当打包一个插件时,Grails 的插件包中将不包含以下文件:

如果你的插件需要 web-app/WEB-INF 目录下的文件,那么建议你修改插件的 scripts/_Install.groovy Gant 脚本文件把项目的目标目录安装到插件包中。

此外, UrlMappings.groovy 文件默认不会避免命名冲突,你可以使用在默认名字前加增加 前缀。比如叫做 grails-app/conf/BlogUrlMappings.groovy

12.5 评估规约

在得以继续查看基于规约所能提供的运行时配置以前,有必要了解一下怎样来评估插件的这些基本规约。本质上,每一个插件都有一个隐含的 GrailsApplication接口的实例变量:application

GrailsApplication 提供了在工程内评估这些规约的方法并且保存着所有类的相互引用,这些类都实现了 GrailsClass 接口.

一个 GrailsClass 代表着一个物理的Grails资源,比如一个控制器或者一个标签库。如果要获取所有 GrailsClass 实例,你可以这样:

application.allClasses.each { println it.name }

GrailsApplication 实例中有一些特殊的属性可以方便的操作你感兴趣的人工制品(artefact)类型,比如你要获取所有控制器的类,可以如此:

application.controllerClasses.each { println it.name }

这些动态方法的规约如下:

GrailsClass 接口本身也提供了很多有用的方法以允许你进一步的评估和了解这些规约,他们包括:

完整的索引请参考 javadoc API.

12.6 参与构建事件

安装后进行配置和参与升级操作

Grails插件可以在安装完后进行配置并且可以参与应用的升级过程(通过 upgrade命令),这是由scripts目录下两个特定名称的脚本来完成的: - _Install.groovy_Upgrade.groovy.

_Install.groovy 是在插件安装完成后被执行的,而 _Upgrade.groovy 是用户每次通过 upgrade 命令来升级他的应用时被执行的.

这些是一个普通的 Gant 脚本,因此你完全可以使用Gant的强大特性。另外 pluginBasedir 被加入到Gant的标准变量中,其指向安装插件的根目录。

以下的 _Install.groovy 示例脚本将在 grails-app 目录下创建一个新的目录,并且安装一个配置模板,如下:

Ant.mkdir(dir:"${basedir}/grails-app/jobs")
Ant.copy(file:"${pluginBasedir}/src/samples/SamplePluginConfiguration.groovy",
         todir:"${basedir}/grails-app/conf")

// To access Grails home you can use following code: // Ant.property(environment:"env") // grailsHome = Ant.antProject.properties."env.GRAILS_HOME"

脚本事件

将插件和命令行的脚本事件关联起来还是有可能的,这些事件在执行Grails的任务和插件事件的时候被触发。

比如你希望在更新的时候,显示更新状态(如"Tests passed", "Server running"),并且创建文件或者人工制品。

一个插件只能通过 Events.groovy 脚本来监听那些必要的事件。 更多详细信息请参考 Hooking into Events.

12.7 运行时配置中的钩子Hooking into Runtime Configuration

Grails提供了很多的钩子函数来处理系统的不同部分,并且通过惯例的形式来执行运行时配置。

跟Grails的Spring配置进行交互

首先你可以使用 doWithSpring 闭包来跟Grails运行时的配置进行交互,例如下面的代码片段是取自于Grails核心插件 i18n的一部分:

import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

class I18nGrailsPlugin {

def version = 0.1

def doWithSpring = { messageSource(ReloadableResourceBundleMessageSource) { basename = "WEB-INF/grails-app/i18n/messages" } localeChangeInterceptor(LocaleChangeInterceptor) { paramName = "lang" } localeResolver(CookieLocaleResolver) } }

这个插件建立起了Grails messageSource bean和一对其他beans以管理Locale解释和更改。它使用 Spring Bean Builder 语法。

参与web.xml的生成

Grails是在加载的时候生成 WEB-INF/web.xml 文件,因此插件不能直接修改此文件,但他们可以参与此文件的生成。 本质上一个插件可以通过 doWithWebDescriptor 闭包来完成此功能,此闭包的参数是 web.xml 是作为 XmlSlurper GPathResult类型传入的.

考虑如下来自 ControllersPlugin的示例:

def doWithWebDescriptor = { webXml ->
        def mappingElement = webXml.'servlet-mapping'
        mappingElement + {
                'servlet-mapping' {
                        'servlet-name'("grails")
                        'url-pattern'("*.dispatch")
                }
        }
}

此处插件得到最后一个 <servlet-mapping>元素的引用, 并且在其后添加Grails' servlet,这得益于XmlSlurper可以通过闭包以编程的方式修改XML的能力。

在初始化完毕后进行配置

有时候在Spring的 ApplicationContext 被创建以后做一些运行时配置是有意义的,这种情况下,你可以定义 doWithApplicationContext 闭包,如下例:

class SimplePlugin {
     def name="simple"
     def version = 1.1

def doWithApplicationContext = { appCtx -> SessionFactory sf = appCtx.getBean("sessionFactory") // do something here with session factory } }

12.8 运行时添加动态方法

基础知识

Grails插件允许你在运行时注册Grails管辖类或者其他类的动态方法,但新的方法只能通过 doWithDynamicMethods 闭包来增加。

对Grails管辖类来说,比如controllers、tag libraries等等,你可以增加方法,构造函数等,这是通过 ExpandoMetaClass 机制做到的,比如访问每个控制器的 MetaClass的代码如下所示:

class ExamplePlugin {
  def doWithDynamicMethods = { applicationContext ->
        application.controllerClasses.each { controllerClass ->
             controllerClass.metaClass.myNewMethod = {-> println "hello world" }
        }
  }
}

此处我们通过隐含的application对象来获取所有控制器类的MetaClass实例,并且为每一个控制器增加一个 myNewMethod 的方法。 或者,你已经知道要处理的类的类型了,那你只需要在此类的 metaClass 属性上增加一个方法即可,代码如下:

class ExamplePlugin {

def doWithDynamicMethods = { applicationContext -> String.metaClass.swapCase = {-> def sb = new StringBuffer() delegate.each { sb << (Character.isUpperCase(it as char) ? Character.toLowerCase(it as char) : Character.toUpperCase(it as char)) } sb.toString() }

assert "UpAndDown" == "uPaNDdOWN".swapCase() } }

此例中,我们直接在 java.lang.StringmetaClass 上增加一个新的 swapCase 方法.

跟ApplicationContext交互

doWithDynamicMethods 闭包的参数是Spring的 ApplicationContext 实例,这点非常有用,因为这允许你和该应用上下文实例中的对象进行交互。比如你打算实现一个跟Hibernate交互的方法,那你可以联合着 HibernateTemplate来使用SessionFactory 例,代码如下:

import org.springframework.orm.hibernate3.HibernateTemplate

class ExampleHibernatePlugin {

def doWithDynamicMethods = { applicationContext ->

application.domainClasses.each { domainClass ->

domainClass.metaClass.static.load = { Long id-> def sf = applicationContext.sessionFactory def template = new HibernateTemplate(sf) template.load(delegate, id) } } } }

另外因为Spring容器具有自动装配和依赖注入的能力,你可以在运行时实现更强大的动态构造器,此构造器使用applicationContext来装配你的对象及其依赖:

class MyConstructorPlugin {

def doWithDynamicMethods = { applicationContext -> application.domainClasses.each { domainClass -> domainClass.metaClass.constructor = {-> return applicationContext.getBean(domainClass.name) } }

} }

这里我们实际做的是通过查找Spring的原型beans(prototyped beans)来替代缺省的构造器。

12.9 参与自动重载

监控资源的改变

通常来讲,当资源发生改变的时候,监控并且重新加载这些变化是非常有意义的。这也是Grails为什么要在运行时实现复杂的应用程序重新加载。查看如下Grails的 ServicesPlugin的一段简单的代码片段:

class ServicesGrailsPlugin {
    …
    def watchedResources = "file:./grails-app/services/*Service.groovy"

… def onChange = { event -> if(event.source) { def serviceClass = application.addServiceClass(event.source) def serviceName = "${serviceClass.propertyName}" def beans = beans { "$serviceName"(serviceClass.getClazz()) { bean -> bean.autowire = true } } if(event.ctx) { event.ctx.registerBeanDefinition(serviceName, beans.getBeanDefinition(serviceName)) } } } }

首先定义了 watchedResources 集合,此集合可能是String或者String的List,包含着要监控的资源的引用或者模式。 如果要监控的资源是Groovy文件,那当它被改变的时候,此文件将会自动被重新加载,而且被传给 onChange 闭包的参数 event .

event 对象定义了一些有益的属性:

通过这些对象,你可以评估这些惯例,而且基于这些惯例你可以将这些变化适当的应用到 ApplicationContext 中, 在上述的"Services"示例中,当一个service类变化时,一个新的service类被重新注册到 ApplicationContext 中.

影响其他插件

当一个插件变化时,插件不但要有相应地反应,而且有时还会“影响”另外的插件。

以Services 和 Controllers插件为例. 当一个service被重新加载的时候,除非你也重新加载controllers,否则你将加载过的service自动装配到旧的controller类的时候,将会发生问题。.

为了避免这种情况发生,你可以指定将要受到“影响”的另外一个插件,这意味着当一个插件监测到改变的时候,它将先重新加载自身,然后重新加载它所影响到的所有插件。看 ServicesGrailsPlugin的代码片段:

def influences = ['controllers']

观察其他插件

如果你想观察一个特殊的插件的变化但又不需要监视插件的资源,那你可以使用"observe"属性:

def observe = ["hibernate"]

在此示例中,当一个Hibernate的领域类变化的时候,你将收到从hibernate插件传递过来的事件。 你也可以使用一个通配符查看所有加载的插件:

def observe = ["*"]

Logging plugin不仅如此,当应用运行时它都能添加 log 属性到 任何 插件库。

12.10 理解插件加载的顺序

Controlling Plug-in Dependencies

插件经常依赖于其他已经存在的插件,并且也能调整这种依赖. 为了做到这点,一个插件可以定义两个属性,首先是 dependsOn.让我们看看Grails Hibernate插件的代码片段:

class HibernateGrailsPlugin {
        def version = 1.0
        def dependsOn = [dataSource:1.0,
                         domainClass:1.0,
                         i18n:1.0,
                         core: 1.0]

}

如上述示例所演示的,Hibernate插件依赖于4个插件: dataSource , domainClass, i18ncore.

根本上讲,这些被依赖的插件将先被加载,接着才是Hibernate插件,如果这些被依赖的插件没有加载,那么Hibernate也不会加载。

dependsOn属性也支持一个小型的表达语言指定版本范围。以下是一些简单的语法例子:

def dependsOn = [foo:"* > 1.0"]
def dependsOn = [foo:"1.0 > 1.1"]
def dependsOn = [foo:"1.0 > *"]

当使用*通配符的时候,它表示"任何"版本。 The expression syntax also excludes any suffixes such as -BETA, -ALPHA etc. so for example the expression "1.0 > 1.1" would match any of the following versions:

Controlling Load Order

如果所依赖的插件不能被解析的话,则依赖于此的插件将被放弃并且不会被加载,这就是所谓的“强”依赖。 然而我们可以通过使用 loadAfter来定义一个“弱”依赖,示例如下:

def loadAfter = ['controllers']

此处如果 controllers 插件存在的话,插件将在controllers之后被加载,否则的话将被单独加载. 插件也可以适应于其他已存在的插件,以Hibernate插件的 doWithSpring闭包代码为例:

if(manager?.hasGrailsPlugin("controllers")) {
        openSessionInViewInterceptor(OpenSessionInViewInterceptor) {
                flushMode = HibernateAccessor.FLUSH_MANUAL
                sessionFactory = sessionFactory
        }
        grailsUrlHandlerMapping.interceptors << openSessionInViewInterceptor
  }

这里,controllers插件如果被加载的话,Hibernate插件仅仅注册一个OpenSessionInViewInterceptor 变量manager是 GrailsPluginManager interface 接口的一个实例,并且提供同其他插件交互的方法,而且 GrailsPluginManager 本身存在与任何一个插件中。